package com.svndiffdoc;

import java.util.ArrayList;
import java.util.List;

public class LineDiff {
    private static final String STYLE_CLASS_OLD_FRAGMENT_EQUAL   = "oldFragmentEqual";
    private static final String STYLE_CLASS_OLD_FRAGMENT_DELETED = "oldFragmentDeleted";
    private static final String STYLE_CLASS_NEW_FRAGMENT_EQUAL   = "newFragmentEqual";
    private static final String STYLE_CLASS_NEW_FRAGMENT_ADDED   = "newFragmentAdded";

    private final String oldLine;
    private final List<AnnotatedFileLineCharacter> oldLineCharacters = new ArrayList<AnnotatedFileLineCharacter>();
    private final List<AnnotatedFileLineFragment> oldLineFragments = new ArrayList<AnnotatedFileLineFragment>();

    private final String newLine;
    private final List<AnnotatedFileLineCharacter> newLineCharacters = new ArrayList<AnnotatedFileLineCharacter>();
    private final List<AnnotatedFileLineFragment> newLineFragments = new ArrayList<AnnotatedFileLineFragment>();

    public LineDiff(String oldLine, String newLine) {
        this.oldLine = oldLine;
        this.newLine = newLine;
        generateDiff();
    }

    private void generateDiff() {
        if (oldLine != null) {
            for (char c : oldLine.toCharArray()) {
                oldLineCharacters.add(new AnnotatedFileLineCharacter(c));
            }
        }

        if (newLine != null) {
            for (char c : newLine.toCharArray()) {
                newLineCharacters.add(new AnnotatedFileLineCharacter(c));
            }
        }

        int N = oldLineCharacters.size();
        int M = newLineCharacters.size();

        // build diff array from old and new line
        if ((M > 0) && (N > 0)) {

            // opt[i][j] = length of LCS of x[i..M] and y[j..N]
            int[][] opt = new int[M+1][N+1];

            // compute length of LCS and all subproblems via dynamic programming
            for (int i = M-1; i >= 0; i--) {
                for (int j = N-1; j >= 0; j--) {
                    if (newLineCharacters.get(i).equals(oldLineCharacters.get(j))) {
                        opt[i][j] = opt[i+1][j+1] + 1;
                    }
                    else {
                        opt[i][j] = Math.max(opt[i+1][j], opt[i][j+1]);
                    }
                }
            }

            // select match point chars
            int i = 0, j = 0;
            while((i < M) && (j < N)) {
                if (newLineCharacters.get(i).equals(oldLineCharacters.get(j))) {
                    newLineCharacters.get(i++).setMatchPoint(true);
                    oldLineCharacters.get(j++).setMatchPoint(true);
                }

                else if (opt[i+1][j] >= opt[i][j+1]) {
                    newLineCharacters.get(i++).setMatchPoint(false);
                }
                else {
                    oldLineCharacters.get(j++).setMatchPoint(false);
                }
            }

            while(i < M) {
                newLineCharacters.get(i++).setMatchPoint(false);
            }

            while(j < N) {
                oldLineCharacters.get(j++).setMatchPoint(false);
            }

            // build fragments
            oldLineFragments.addAll(buildLineFragments(oldLineCharacters, STYLE_CLASS_OLD_FRAGMENT_EQUAL, STYLE_CLASS_OLD_FRAGMENT_DELETED));
            newLineFragments.addAll(buildLineFragments(newLineCharacters, STYLE_CLASS_NEW_FRAGMENT_EQUAL, STYLE_CLASS_NEW_FRAGMENT_ADDED));
        }

        // build dummy fragments if old line is empty
        else if (M > 0) {
            newLineFragments.addAll(buildLineFragments(newLineCharacters, STYLE_CLASS_NEW_FRAGMENT_EQUAL, STYLE_CLASS_NEW_FRAGMENT_ADDED));
        }

        // build dummy fragments if new line is empty
        else if (N > 0) {
            oldLineFragments.addAll(buildLineFragments(oldLineCharacters, STYLE_CLASS_OLD_FRAGMENT_EQUAL, STYLE_CLASS_OLD_FRAGMENT_DELETED));
        }
    }

    private List<AnnotatedFileLineFragment> buildLineFragments(List<AnnotatedFileLineCharacter> characters, String matchPointClassStyle, String nonMatchPointClassStyle) {
        List<AnnotatedFileLineFragment> lineFragments = new ArrayList<AnnotatedFileLineFragment>();
        boolean isMatchPoint = true;
        boolean isPrevMatchPoint = true;
        StringBuffer charBuffer = new StringBuffer();
        for (AnnotatedFileLineCharacter character : characters) {
            isMatchPoint = character.isMatchPoint();
            if (isMatchPoint == isPrevMatchPoint) {
                charBuffer.append(character.getCharacter());
            }
            else {
                lineFragments.add(new AnnotatedFileLineFragment(charBuffer.toString(), isPrevMatchPoint, isPrevMatchPoint ? matchPointClassStyle : nonMatchPointClassStyle));
                // reset char buffer
                isPrevMatchPoint = isMatchPoint;
                charBuffer = new StringBuffer().append(character.getCharacter());
            }
        }
        // add last fragment
        lineFragments.add(new AnnotatedFileLineFragment(charBuffer.toString(), isPrevMatchPoint, isPrevMatchPoint ? matchPointClassStyle : nonMatchPointClassStyle));
        return lineFragments;
    }

    public List<AnnotatedFileLineFragment> getOldLineFragments() {
        return oldLineFragments;
    }

    public List<AnnotatedFileLineFragment> getNewLineFragments() {
        return newLineFragments;
    }

}
